package org.glassunit.core;

import org.glassunit.core.jdbc.JdbcResourceSettings;
import org.glassunit.core.jdbc.JdbcConnectionPoolSettings;
import com.sun.enterprise.deploy.shared.ArchiveFactory;
import java.io.File;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.naming.Binding;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingEnumeration;
import org.apache.derby.jdbc.EmbeddedDataSource;
import org.glassfish.api.ActionReport.ExitCode;
import org.glassfish.api.deployment.DeployCommandParameters;
import org.glassfish.api.deployment.archive.ReadableArchive;
import org.glassfish.api.embedded.ContainerBuilder.Type;
import org.glassfish.api.embedded.EmbeddedFileSystem;
import org.glassfish.api.embedded.LifecycleException;
import org.glassfish.api.embedded.Server;
import org.glassfish.api.embedded.admin.CommandExecution;
import org.glassfish.api.embedded.admin.EmbeddedAdminContainer;
import org.glassunit.core.deploy.Module;
import org.glassunit.core.util.FileUtils;

/**
 * GlassUnit main class.
 * @author nocturne
 */
public class GlassUnit {

    private Server server;
    private Context context;
    private File rootFile;
    private File domainFile;
    private File applicationFile;

    /**
     * Create a new GlassUnit instance without web connector.
     */
    public GlassUnit() {
        this(null);
    }

    /**
     * Create a new GlassUnit instance with web connector.
     * @param port The connector's port
     */
    public GlassUnit(final Integer port) {
        try {
            rootFile = FileUtils.createDirectory(File.createTempFile("glassunit", ""));
            domainFile = FileUtils.createDirectory(new File(rootFile, "domain"));
            applicationFile = FileUtils.createDirectory(new File(rootFile, "applications"));

            Logger.getAnonymousLogger().info(String.format("Initializng GlassUnit(%s)", rootFile));

            final EmbeddedFileSystem.Builder fileSystemBuilder = new EmbeddedFileSystem.Builder();
            fileSystemBuilder.installRoot(applicationFile, true);
            fileSystemBuilder.instanceRoot(domainFile);
            fileSystemBuilder.autoDelete(false);

            final Server.Builder serverBuilder = new Server.Builder("GlassUnit");
            serverBuilder.embeddedFileSystem(fileSystemBuilder.build());
            serverBuilder.verbose(true);
            server = serverBuilder.build();
            server.addContainer(Type.ejb);
            server.addContainer(Type.web);
            if (port != null) {
                server.createPort(port);
            }
        } catch (final Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Gets the instance's root directory.
     * @return root directory
     */
    public File getRootFile() {
        return rootFile;
    }

    /**
     * Gets the embedded Glassfish server.
     * @return Glassfish server
     */
    public Server getServer() {
        return server;
    }

    /**
     * Adds a new in memory derby jdbc connection pool.
     * @param name pool name
     */
    public void addJdbcConnectionPool(final String name) {
        final JdbcConnectionPoolSettings settings = new JdbcConnectionPoolSettings();
        settings.addOption(JdbcConnectionPoolSettings.DATASOURCE_CLASSNAME, EmbeddedDataSource.class.getName());
        settings.addOption(JdbcConnectionPoolSettings.POOL_ID, name);
        settings.addOption(JdbcConnectionPoolSettings.PROPERTY, "databasename='memory:" + name + "':connectionAttributes=';create=true'");
        addJdbcConnectionPool(settings);
    }

    /**
     * Adds a new jdbc connection pool with the given settings.
     * @param settings pool settings
     */
    public void addJdbcConnectionPool(final JdbcConnectionPoolSettings settings) {
        final EmbeddedAdminContainer adminContainer = server.getHabitat().getByContract(EmbeddedAdminContainer.class);
        final CommandExecution execution = adminContainer.execute("create-jdbc-connection-pool", settings);
        if (execution.getExitCode() == ExitCode.FAILURE) {
            throw new RuntimeException(execution.getMessage());
        }
    }

    /**
     * Adds a new jdbc resource with the given name and pool.
     * @param poolName pool name
     * @param jndiName resource jndi name
     */
    public void addJdbcResource(final String poolName, final String jndiName) {
        final JdbcResourceSettings settings = new JdbcResourceSettings();
        settings.addOption(JdbcResourceSettings.JNDI_NAME, jndiName);
        settings.addOption(JdbcResourceSettings.POOL_ID, poolName);
        addJdbcResource(settings);
    }

    /**
     * Adds a new jdbc resource with the given settings.
     * @param settings resource settings
     */
    public void addJdbcResource(final JdbcResourceSettings settings) {
        final EmbeddedAdminContainer adminContainer = server.getHabitat().getByContract(EmbeddedAdminContainer.class);
        final CommandExecution execution = adminContainer.execute("create-jdbc-resource", settings);
        if (execution.getExitCode() == ExitCode.FAILURE) {
            throw new RuntimeException(execution.getMessage());
        }
    }

    /**
     * Stats the GlassUnit instance.
     */
    public void start() {
        try {
            server.start();
            context = new InitialContext();
        } catch (final Exception ex) {
            throw new RuntimeException("Could not start GlassUnit instance", ex);
        }
    }

    /**
     * Deploys the given module.
     * @param module module
     */
    public void deploy(final Module module) {
        deploy(createModuleArchive(module));
    }

    /**
     * Creates a readable archive for the given module.
     * @param module module
     * @return readable archive
     */
    private ReadableArchive createModuleArchive(final Module module) {
        try {
            final File root = FileUtils.createDirectory(new File(applicationFile, module.getName()));

            for (String directory : module.getDirectories()) {
                FileUtils.createDirectory(new File(root, directory));
            }

            for (String file : module.getFiles()) {
                FileUtils.saveFile(module.getFile(file), new File(root, file));
            }

            final ArchiveFactory archiveFactory = server.getHabitat().getComponent(ArchiveFactory.class);
            return archiveFactory.openArchive(root);
        } catch (final IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Deploys the given archive.
     * @param archive archive
     */
    public void deploy(final ReadableArchive archive) {
        final DeployCommandParameters parameters = new DeployCommandParameters();
        parameters.logReportedErrors = true;
        parameters.path = applicationFile;
        deploy(archive, parameters);
    }

    /**
     * Deploys the given archive using the given deployment parameters.
     * @param archive archive
     * @param parameters deployment parameters
     */
    public void deploy(final ReadableArchive archive, final DeployCommandParameters parameters) {
        server.getDeployer().deploy(archive, parameters);
    }

    /**
     * Gets the first ejb isntance with the given interface from the root jndi context.
     * @param <EJB>  ejb type
     * @param ejbInterface ejb interface
     * @return ejb instance
     */
    public <EJB> EJB getBean(final Class<EJB> ejbInterface) {
        final EJB bean = getInstance(ejbInterface, "", context);
        if (bean == null) {
            throw new RuntimeException(String.format("Could not find Bean for interface %s", ejbInterface.getName()));
        }
        return bean;
    }

    /**
     * Gets the first ejb with the given interface from the given jndi context.
     * @param <EJB> ejb type
     * @param ejbInterface ejb interface
     * @param prefix prefix
     * @param subContext sub context
     * @return ejb instance
     */
    private <EJB> EJB getInstance(final Class<EJB> ejbInterface, final String prefix, final Context subContext) {
        try {
            final NamingEnumeration<Binding> bindings = subContext.listBindings("");
            EJB result = null;
            while (result == null && bindings.hasMore()) {
                final Binding binding = bindings.next();
                if (binding.getObject() instanceof Context) {
                    result = getInstance(ejbInterface, prefix + binding.getName() + "/", (Context) binding.getObject());
                } else if (binding.getName().endsWith(ejbInterface.getName())) {
                    result = (EJB) context.lookup(prefix + binding.getName());
                }
            }
            return result;
        } catch (final Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    /**
     * Gets the jndi context.
     * @return jndi context
     */
    public Context getContext() {
        return context;
    }

    /**
     * Prints the jndi context to console.
     */
    public void printContext() {
        printContext(0, context);
    }

    /**
     * Prints the given context to console.
     * @param indent indent
     * @param context context
     */
    private void printContext(final int indent, final Context context) {
        try {
            final NamingEnumeration<Binding> list = context.listBindings("");
            while (list.hasMore()) {
                final Binding binding = list.next();
                for (int index = 0; index < indent; index++) {
                    System.out.print("\t");
                }

                System.out.print(binding.getName() + ": ");
                if (binding.getObject() instanceof Context) {
                    System.out.println("Context");
                    printContext(indent + 1, (Context) binding.getObject());
                } else {
                    System.out.println(binding.getClassName());
                }

            }
        } catch (final Exception ex) {
            Logger.getAnonymousLogger().log(Level.WARNING, "Could not read context " + context);
        }
    }

    /**
     * Stops the GlassUnit instance.
     */
    public void stop() {
        try {
            server.stop();
            
            context = null;
            int retry = 0;
            while (retry < 5 && rootFile.exists()) {
                retry++;
                if (!FileUtils.deleteFileRecursive(rootFile)) {
                    try {
                        Thread.sleep(100);
                    } catch (final InterruptedException ex) {
                        Logger.getLogger(GlassUnit.class.getName()).log(Level.SEVERE, null, ex);
                    }
                }
            }
        } catch (final LifecycleException ex) {
            throw new RuntimeException("Could not stop GlassUnit instance", ex);
        }
    }
}
